package searcher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 通过这个类在内存中来构造出索引结构

/**
 * 正排索引，每个list对象都是一个文档，包含处理过的标题，URL，content
 * 倒排索引，每个对象包含一个分好词后的String对象，以及包含该词的docId,以及文档中对应的权重(标题出现的次数 * 10 +
 * 正文中出现的次数)
 */
public class Index {
    // 修改为本地的java文档存储目录以及索引存储目录
    private static final String INDEX_PATH = "/Users/liuyiming/Documents/Git_Project/bit_project/" +
            "doc_searcher/doc_searcher_index/";
    private ObjectMapper objectMapper = new ObjectMapper();

    // 使用数组下标表示 docId
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 使用 哈希表 来表示倒排索引
    // key 就是词
    // value 就是一组和这个词关联的文章
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 新创建两个锁对象
    private Object locker1 = new Object();
    private Object locker2 = new Object();

    // 这个类要提供的方法:
    // 1. 给定一个 docId, 在正排索引中, 查询文档的详细信息.
    public DocInfo getDocInfo(int docId) {
        return forwardIndex.get(docId);
    }
    // 返回的列表中包含了关联这个词的文档以及该文档与该词的相关性 - weight
    // 2. 给定一个词, 在倒排索引中, 查哪些文档和这个词关联.
    //    仔细思考这里的返回值. 单纯的返回一个 整数 的 List 是否可行呢? 这样不太好
    //    词和文档之间是存在一定的 "相关性" 的.
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    // 3. 往索引中新增一个文档.
    public void addDoc(String title, String url, String content) {
        // 新增文档操作, 需要同时给正排索引和倒排索引新增信息
        // 构建正排索引
        DocInfo docInfo = buildForward(title, url, content);
        // 构建倒排索引
        buildInverted(docInfo);
    }

    /**
     * 构建倒排索引，词 =》文档id的映射关系
     * 首先需要知道当前文档包含哪些词(标题和正文)
     * 此处我们倒排索引的key为分词后的每个词，value为该词对应的关联文档(文档id，该文档的权重)
     * 权重现在就是单纯的统计该文档中分词出现的词频～
     * 这里分词后不区分大小写
     * @param docInfo
     */
    private void buildInverted(DocInfo docInfo) {
        // 方法内部类，分别保存该词在标题和正文中出现的次数
        class WordCnt {
            // 表示这个词在标题中出现的次数
            public int titleCount;
            // 表示这个词在正文中出现的次数
            public int contentCount;
        }
        // 这个数据结构用来统计词频
        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();

        // 1. 针对文档标题进行分词.
        // 拿到的分词结果就是转换为全小写的
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        // 2. 遍历分词结果, 统计每个词出现的次数.
        for (Term term : terms) {
            // 先判定一下 term 是否存在.
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            // 此时该word是第一次出现，创建新的WordCount对象保存
            if (wordCnt == null) {
                // 如果不存在, 就创建一个新的键值对, 插入进去. titleCount 设为 1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                // 如果存在, 就找到之前的值, 然后把对应的 titleCount + 1
                wordCnt.titleCount += 1;
            }
        }
        // 3. 针对正文页进行分词.
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        // 4. 遍历分词结果, 统计每个词出现的次数.
        for (Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 0;
                newWordCnt.contentCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                wordCnt.contentCount += 1;
            }
        }
        // 5. 把上面的结果汇总到一个 HashMap 里面.
        //    最终文档的权重, 就设定成标题中出现的次数 * 10 + 正文中出现的次数.
        // 在实际工作中，改进权重公式的思路 : 往往通过"点击率" = 点击次数 / 展示次数来衡量不同策略的点击率高低来不断
        // 优化权重公式
        // 6. 遍历刚才这个 HashMap, 依次来更新倒排索引中的结构了.
        for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
            // 先根据这里的词, 去倒排索引中查一查~
            // 倒排拉链
            // 多线程写倒排索引文件需要保证写操作的线程安全，只在写操作上加锁，保证锁的粒度较细
            synchronized (locker2) {
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                if (invertedList == null) {
                    // 如果为空, 就插入一个新的键值对
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    // 把新的文档(当前 searcher.DocInfo), 构造成 searcher.Weight 对象, 插入进来.
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 权重计算公式: 标题中出现的次数 * 10 + 正文中出现的次数
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    invertedIndex.put(entry.getKey(), newInvertedList);
                } else {
                    // 如果非空, 就把当前这个文档, 构造出一个 searcher.Weight 对象, 插入到倒排拉链的后面
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 权重计算公式: 标题中出现的次数 * 10 + 正文中出现的次数
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    invertedList.add(weight);
                }
            }
        }
    }

    /**
     * 构建正排索引
     * @param title
     * @param url
     * @param content
     * @return
     */
    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        // 写正排索引文件需要保证多线程场景下的线程安全
        synchronized (locker1) {
            // 新加入的doc实体尾插到forwardIndex
            // 因此插入后他的索引恰好为插入前forwardIndex的长度
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

    // 4. 构建索引的过程是比较耗时的，不应该在服务器启动时才构建索引，应该在启动服务之前就构建好
    // 并且也不能每次启动服务都重新构建索引
    // 因此应该将耗时操作单独执行，启动服务只需要从磁盘加载构建好的索引即可
    // 将内存中索引结构保存到磁盘的操作 =》 就是将索引序列化为字符串 json
    // 把内存中的索引结构保存到磁盘中.
    // json序列化操作
    public void save() {
        // 使用两个文件, 分别保存正排和倒排
        long beg = System.currentTimeMillis();
        System.out.println("保存索引开始!");
        // 1. 先判定一下索引对应的目录是否存在, 不存在就创建.
        File indexPathFile = new File(INDEX_PATH);
        if (!indexPathFile.exists()) {
            indexPathFile.mkdirs();
        }
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            // 调用jackson，直接将内存中的正排和倒排索引以json字符串的方式写入到磁盘上的指定文件中
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引完成! 消耗时间: " + (end - beg) + " ms");
    }

    // 5. 把磁盘中的索引数据加载到内存中.
    // json反序列化操作
    public void load() {
        long beg = System.currentTimeMillis();
        System.out.println("加载索引开始!");
        // 1. 先设置一下加载索引的路径
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            // 调用jackson的反序列化方法读取磁盘中的正排索引和倒排索引内容到内存中(借助TypeReference直接将json串反序列化为指定对象)
            // new TypeReference<ArrayList<DocInfo>>() {} 就是一个匿名内部类，就是产生一个TypeReference的对象，
            // 将文件中内容转换为指定TypeReference对象
            // 此处的转换一定正确，因为在序列化时使用相同的类型写入文件
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("加载索引结束! 消耗时间: " + (end - beg) + " ms");
    }

    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("索引加载完成");
    }
}
